﻿//===============================================================================
// Jammer.NET Development Library
// Jmr.DependencyInjection
//===============================================================================
// Copyright © Junaid Fakhruddin, Matthew Leedham and Randolph Cabral.  
// All rights reserved.
//===============================================================================
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;

namespace Jmr.DependencyInjection
{
	public class DependencyResolver<TMapper> 
		where TMapper : DependencyMapper
	{
		public DependencyResolver() 
		{
			DependencyMapper = Activator.CreateInstance<TMapper>();

			if (SingletonObjectCache == null)
				SingletonObjectCache = new Dictionary<string, object>();
			
			if (FileDateModifiedObjectCache == null)
				FileDateModifiedObjectCache = new Dictionary<string, FileDateModifiedObject>();
			
			if (TimeSpanObjectCache == null)
				TimeSpanObjectCache = new Dictionary<string, TimeSpanObject>();
		}

		private TMapper DependencyMapper { get; set; }

		private static Dictionary<string, object> SingletonObjectCache { get; set; }
		private static Dictionary<string, FileDateModifiedObject> FileDateModifiedObjectCache { get; set; }
		private static Dictionary<string, TimeSpanObject> TimeSpanObjectCache { get; set; }

		public static T GetInstance<T>()
		{
			T ret = default(T);
			var dmapper = new DependencyResolver<TMapper>();
			var map = dmapper.DependencyMapper.GetMap<T>();
            string uniqueName = "Jmr.DependencyInjection." + map.ConcreteType.Name;

			switch (map.CacheScope)
			{
				case CacheScopeType.None:
					ret = (T)dmapper.GetObjectFromCacheScopeTypeNone(map);
					break;
				case CacheScopeType.Singleton:
					ret = (T)dmapper.GetObjectFromCacheScopeTypeSingleton(map);
					break;
				case CacheScopeType.ManagedThread:
					ret = (T)dmapper.GetObjectFromCacheScopeTypeManagedThread(map, uniqueName);
					break;
				case CacheScopeType.TimeSpan:
					ret = (T)dmapper.GetObjectFromCacheScopeTypeTimeSpan(map);
					break;
				case CacheScopeType.FileDateModified:
					ret = (T)dmapper.GetObjectFromCacheScopeTypeFileDateModified(map);
					break;
				case CacheScopeType.HttpApplication:
					ret = (T)dmapper.GetObjectFromCacheScopeTypeHttpApplication(map, uniqueName);
					break;
				case CacheScopeType.HttpSession:
					ret = (T)dmapper.GetObjectFromCacheScropTypeHttpSession(map, uniqueName);
					break;
			}
			
			dmapper.ResolveDependencies(ret);
			return ret;
		}

		public static void ResolveInstance<T>(T instance)
		{
			var dmapper = new DependencyResolver<TMapper>();
			dmapper.ResolveDependencies(instance);
		}

		public void ResolveDependencies<T>(T instance)
		{
			var props = instance.GetType().GetProperties();
			foreach (var prop in props)
			{
				var map = DependencyMapper.GetMap(prop.PropertyType);
				if (map != null)
					InjectDependency(map, prop, instance);
			}
		}

		public void InjectDependency<T>(DependencyMap map, PropertyInfo propinfo, T instance)
		{
			string uniqueName = "Jmr.DependencyInjection." + map.ConcreteType.Name;
			object objectToInject = null;

			switch (map.CacheScope)
			{
				case CacheScopeType.None:
					objectToInject = GetObjectFromCacheScopeTypeNone(map);
					break;
				case CacheScopeType.Singleton:
					objectToInject = GetObjectFromCacheScopeTypeSingleton(map);
					break;
				case CacheScopeType.ManagedThread:
					objectToInject = GetObjectFromCacheScopeTypeManagedThread(map, uniqueName);
					break;
				case CacheScopeType.TimeSpan:
					objectToInject = GetObjectFromCacheScopeTypeTimeSpan(map);
					break;
				case CacheScopeType.FileDateModified:
					objectToInject = GetObjectFromCacheScopeTypeFileDateModified(map);
					break;
				case CacheScopeType.HttpApplication:
					objectToInject = GetObjectFromCacheScopeTypeHttpApplication(map, uniqueName);
					break;
				case CacheScopeType.HttpSession:
					objectToInject = GetObjectFromCacheScropTypeHttpSession(map, uniqueName);
					break;
			}

			ResolveDependencies(objectToInject);
			propinfo.SetValue(instance, objectToInject, null);
		}

		private class TimeSpanObject
		{
			public object Object { get; set; }
			public DateTime LastAccessedDate { get; set; }
		}

		private class FileDateModifiedObject
		{
			public object Object { get; set; }
			public DateTime LastModifiedDate { get; set; }
		}

		private object GetObjectFromCacheScopeTypeNone(DependencyMap map)
		{
			return Activator.CreateInstance(map.ConcreteType);
		}

		private object GetObjectFromCacheScopeTypeSingleton(DependencyMap map)
		{
            object ret = null;
			
            var keyValue = SingletonObjectCache.SingleOrDefault(e => e.Key == map.ConcreteType.Name);
//            if (keyValue != null)
            if (!(default(KeyValuePair<string, object>).Equals(keyValue)))
                ret = keyValue.Value;

            if (ret == null)
			{
				ret = Activator.CreateInstance(map.ConcreteType);
				SingletonObjectCache.Add(map.ConcreteType.Name, ret);
			}
			return ret;
		}

		private object GetObjectFromCacheScopeTypeManagedThread(DependencyMap map, string uniqueName)
		{
			object ret = null;
			var dataslot = Thread.GetNamedDataSlot(uniqueName);
			
			if (dataslot == null)
				Thread.AllocateNamedDataSlot(uniqueName);

			ret = Thread.GetData(dataslot);
			if (ret == null)
			{
				ret = Activator.CreateInstance(map.ConcreteType);
				Thread.SetData(dataslot, ret);
			}
			return ret;
		}

		private object GetObjectFromCacheScopeTypeTimeSpan(DependencyMap map)
		{
			object ret = null;
			var tso = TimeSpanObjectCache.SingleOrDefault(e => e.Key == map.ConcreteType.Name);

			if (!string.IsNullOrEmpty(tso.Key))
			{
				var elapsedtime = DateTime.Now.Subtract(tso.Value.LastAccessedDate);
				if (elapsedtime > map.TimeSpan)
					TimeSpanObjectCache.Remove(map.ConcreteType.Name);
				else
				{
					ret = tso.Value.Object;
					tso.Value.LastAccessedDate = DateTime.Now;
				}
			}
			
			if (ret == null)
			{
				ret = Activator.CreateInstance(map.ConcreteType);
				TimeSpanObjectCache.Add(map.ConcreteType.Name, new TimeSpanObject { Object = ret, LastAccessedDate = DateTime.Now });
			}

			return ret;
		}

		private object GetObjectFromCacheScopeTypeFileDateModified(DependencyMap map)
		{
			object ret = null;
			var fdm = FileDateModifiedObjectCache.SingleOrDefault(e => e.Key == map.ConcreteType.Name);

			if (!string.IsNullOrEmpty(fdm.Key))
			{
				var lastWriteDateTime = File.GetLastWriteTime(map.FilePath);
				if (fdm.Value.LastModifiedDate == lastWriteDateTime)
					ret = fdm.Value.Object;
				else
					FileDateModifiedObjectCache.Remove(map.ConcreteType.Name);
			}

			if (ret == null)
			{
				ret = Activator.CreateInstance(map.ConcreteType);
				TimeSpanObjectCache.Add(map.ConcreteType.Name, new TimeSpanObject { Object = ret, LastAccessedDate = DateTime.Now });
			}

			return ret;
		}

		private object GetObjectFromCacheScopeTypeHttpApplication(DependencyMap map, string uniqueName)
		{
			object ret = HttpContext.Current.Application.Get(uniqueName);
			if (ret == null)
			{
				ret = Activator.CreateInstance(map.ConcreteType);
				HttpContext.Current.Application.Add(uniqueName, ret);
			}
			return ret;
		}

		private object GetObjectFromCacheScropTypeHttpSession(DependencyMap map, string uniqueName)
		{
			object ret = null;
			ret = HttpContext.Current.Session[uniqueName];
			if (ret == null)
			{
				ret = Activator.CreateInstance(map.ConcreteType);
				HttpContext.Current.Session.Add(uniqueName, ret);
			}
			return ret;
		}
	}

	public class DependencyMapper
	{
		public DependencyMapper()
		{
			DependencyMaps = new List<DependencyMap>();
		}

		public List<DependencyMap> DependencyMaps { get; private set; }

		public DependencyMap GetMap<T>()
		{
			return GetMap(typeof(T));
		}

		public DependencyMap GetMap(Type type)
		{
			return DependencyMaps.SingleOrDefault(e => e.ConcreteType.Name == type.Name);
		}

		public DependencyMap RegisterType<T>()
		{
			var map = Activator.CreateInstance<DependencyMap>();
			map.ConcreteType = typeof(T);
			DependencyMaps.Add(map);
			return map;
		}

		public DependencyMap RegisterInterface<T>()
		{
			var map = Activator.CreateInstance<DependencyMap>();
			map.InterfaceType = typeof(T);
			DependencyMaps.Add(map);
			return map;
		}
	}

	public class DependencyMap
	{
		public DependencyMap()
		{
			CacheScope = CacheScopeType.None;
		}

		public Type ConcreteType { get; set; }
		public Type InterfaceType { get; set; }
		public string FilePath { get; set; }
		public CacheScopeType CacheScope { get; set; }
		public TimeSpan TimeSpan { get; set; }

		public DependencyMap SetConcreteType<T>()
		{
			ConcreteType = typeof(T);
			return this;
		}

		public DependencyMap CacheToScope(CacheScopeType scope)
		{
			CacheScope = scope;
			return this;
		}

		public DependencyMap CacheToTimeSpan(TimeSpan timespan)
		{
			CacheScope = CacheScopeType.TimeSpan;
			TimeSpan = timespan;
			return this;
		}

		public DependencyMap CacheToFileDateModified(string path)
		{
			CacheScope = CacheScopeType.FileDateModified;
			FilePath = path;
			return this;
		}
	}

	public enum CacheScopeType
	{
		None,
		Singleton,
		ManagedThread,
		TimeSpan,
		FileDateModified,
		HttpApplication,
		HttpRequest,
		HttpSession,
	}

}
